// Note: The code in this file does not use the 'dst' output parameter of functions in the
// 'wgpu-matrix' library, so produces many temporary vectors and matrices.
// This is intentional, as this sample prefers readability over performance.
import { Mat4, Vec3, Vec4, mat4, vec3 } from 'wgpu-matrix';
import { InputSystem } from '../io/InputSystem';

// Common interface for camera implementations
export default interface Camera {

    // The camera matrix.
    // This is the inverse of the view matrix.
    matrix: Mat4;
    // Alias to column vector 0 of the camera matrix.
    right: Vec4;
    // Alias to column vector 1 of the camera matrix.
    up: Vec4;
    // Alias to column vector 2 of the camera matrix.
    back: Vec4;
    // Alias to column vector 3 of the camera matrix.
    position: Vec4;
}

// The common functionality between camera implementations
class CameraBase {

    // The camera matrix
    private matrix_ = new Float32Array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1,]);

    // The calculated view matrix
    private readonly view_ = mat4.create();

    // Aliases to column vectors of the matrix
    private right_ = new Float32Array(this.matrix_.buffer, 4 * 0, 4);
    private up_ = new Float32Array(this.matrix_.buffer, 4 * 4, 4);
    private back_ = new Float32Array(this.matrix_.buffer, 4 * 8, 4);
    private position_ = new Float32Array(this.matrix_.buffer, 4 * 12, 4);

    zoom = 1;

    // Returns the camera matrix
    get matrix() {
        return this.matrix_;
    }
    // Assigns `mat` to the camera matrix
    set matrix(mat: Mat4) {
        mat4.copy(mat, this.matrix_);
    }

    // Returns the camera view matrix
    get view() {
        return this.view_;
    }
    // Assigns `mat` to the camera view
    set view(mat: Mat4) {
        mat4.copy(mat, this.view_);
    }

    // Returns column vector 0 of the camera matrix
    get right() {
        return this.right_;
    }
    // Assigns `vec` to the first 3 elements of column vector 0 of the camera matrix
    set right(vec: Vec3) {
        vec3.copy(vec, this.right_);
    }

    // Returns column vector 1 of the camera matrix
    get up() {
        return this.up_;
    }
    // Assigns `vec` to the first 3 elements of column vector 1 of the camera matrix
    set up(vec: Vec3) {
        vec3.copy(vec, this.up_);
    }

    // Returns column vector 2 of the camera matrix
    get back() {
        return this.back_;
    }
    // Assigns `vec` to the first 3 elements of column vector 2 of the camera matrix
    set back(vec: Vec3) {
        vec3.copy(vec, this.back_);
    }

    // Returns column vector 3 of the camera matrix
    get position() {
        return this.position_;
    }
    // Assigns `vec` to the first 3 elements of column vector 3 of the camera matrix
    set position(vec: Vec3) {
        vec3.copy(vec, this.position_);
    }


}


export class OrthographicCamera extends CameraBase implements Camera {

    target: Vec3;

    projectionMatrix: Mat4;
    panSpeed: number;
    constructor() {

        super();

        this.target = vec3.create();

        this.position = vec3.create(0, 0, 10);

        this.projectionMatrix = mat4.ortho(-1 / this.zoom, 1 / this.zoom, -1 / this.zoom, 1 / this.zoom, 1 / this.zoom, 100);

        this.panSpeed = 1;

    }

    update(input: InputSystem) {

        if (input.isRightMouseDown) {
            const delta = [input.mouseLastX - input.mouseX, input.mouseY - input.mouseLastY, 0];
            vec3.scale(delta, 0.02 * this.panSpeed, delta);
            vec3.add(this.position, delta, this.position);
        }

        if (input.wheelDelta > 0) {

            this.zoom *= 1.1;
            this.panSpeed /= 1.1;
            this.projectionMatrix = mat4.ortho(-1 / this.zoom, 1 / this.zoom, -1 / this.zoom, 1 / this.zoom, 1 / this.zoom, 100);

        } else if (input.wheelDelta < 0) {

            this.zoom /= 1.1;
            this.panSpeed *= 1.1;
            this.projectionMatrix = mat4.ortho(-1 / this.zoom, 1 / this.zoom, -1 / this.zoom, 1 / this.zoom, 1 / this.zoom, 100);

        }

        input.mouseLastX = input.mouseX;
        input.mouseLastY = input.mouseY;
        input.wheelDelta = 0;

        vec3.sub(this.position, vec3.create(0, 0, 1), this.target);
        mat4.lookAt(this.position, this.target, this.up, this.view);
        return this.view;

    }

}

// Returns `x` clamped between [`min` .. `max`]
function clamp(x: number, min: number, max: number): number {
    return Math.min(Math.max(x, min), max);
}

// Returns `x` float-modulo `div`
function mod(x: number, div: number): number {
    return x - Math.floor(Math.abs(x) / div) * div * Math.sign(x);
}

// Returns `vec` rotated `angle` radians around `axis`
function rotate(vec: Vec3, axis: Vec3, angle: number): Vec3 {
    return vec3.transformMat4Upper3x3(vec, mat4.rotation(axis, angle));
}

// Returns the linear interpolation between 'a' and 'b' using 's'
function lerp(a: Vec3, b: Vec3, s: number): Vec3 {
    return vec3.addScaled(a, vec3.sub(b, a), s);
}
